/* 
 * [New BSD License (BSD 3-Clause License)]
 * Copyright (c) 2012, Max Bechtold
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 *     - Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     - Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     - Neither the name of the Rel2Xml Project nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package maxbe.rel2xml.generator.dml;

import static maxbe.rel2xml.util.Config.getBoolean;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;

import maxbe.rel2xml.generator.Table;
import maxbe.rel2xml.generator.dml.predicates.ColumnComparison;
import maxbe.rel2xml.generator.dml.predicates.Predicate;
import maxbe.rel2xml.util.Config;

/**
 * This class holds all components a query is made of. This allows for a generic
 * and adaptable SQL to XQuery translation
 * 
 * Constraints on queries:
 * <ul>
 * <li>The only predicate is a conjunction of one atomic predicate or of
 * disjunctions. Disjunctions can contain one or more atomic predicates</li>
 * <li>There cannot exist more than one join predicate between two tables</li>
 * </ul>
 * 
 * @author Max Bechtold
 * 
 */
public class Query extends AbstractDmlInstance {

	private Projection projection;

	private OrderBy orderBy;

	/** States whether it was possible to join in a hierarchical manner */
	private boolean hierarchical;

	/**
	 * Lists all entities that are part of the query, update expressions only
	 * have one
	 */
	protected LinkedHashMap<String, Entity> entities;

	public Query(String rootNodePath) {
		this.rootNodePath = rootNodePath;
	}

	public Projection getProjection() {
		return projection;
	}

	public OrderBy getOrderBy() {
		return orderBy;
	}

	public Entity getEntity(Table table) {
		for (Entity entity : entities.values())
			if (entity.getTable() == table)
				return entity;
		return null;
	}

	public boolean joinedHierarchically() {
		return hierarchical;
	}

	public LinkedHashMap<String, Entity> getEntities() {
		return entities;
	}

	public void setProjection(Projection projection) {
		projection.setQuery(this);
		this.projection = projection;
	}

	public void setOrderBy(OrderBy orderBy) {
		orderBy.setQuery(this);
		this.orderBy = orderBy;
	}

	public void setHierarchical(boolean hierarchical) {
		this.hierarchical = hierarchical;
	}

	@Override
	public String toXQuery() {
		// Setup components
		connectEntities();
		String query = super.toXQuery();
		String extVars = declareExtVars();
		if (!extVars.isEmpty())
			query += extVars + "\n";

		query += String.format("let $%s := %s\n%s", Config.ROOT_NODE_VAR,
				rootNodePath, getForAndLetClause());

		if (predicate != null) {
			String where = predicate.toXQuery();
			if (!where.isEmpty())
				query += "\nwhere " + where;
		}

		if (orderBy != null)
			query += "\n" + orderBy.toXQuery();

		query += "\n" + projection.toXQuery() + ';';

		return query;
	}

	@Override
	public String toSQL() {
		String result = projection.toSql();

		result += "\nfrom ";
		for (String key : entities.keySet())
			result += entities.get(key).toString() + " " + key + ", ";
		result = result.substring(0, result.length() - 2);

		if (predicate != null)
			result += "\nwhere " + predicate.toString();

		if (orderBy != null)
			result += '\n' + orderBy.toSql();

		return result;
	}

	@Override
	public String toString() {
		return toSQL();
	}

	private void connectEntities() {
		if (predicate == null)
			return;

		hierarchical = getBoolean(Config.HIERARCHICAL_RESULT);

		// Test if predicate manifests join over (implicit) foreign key or if it
		// demands remapping
		// Note: Join predicates have to be elements of the top level
		// conjunction of the query's predicate

		// 1) Keep with document mapping by searching for mapping determining
		// foreign key join predicates
		for (Predicate pred : predicate.getPredicates())
			if (pred instanceof ColumnComparison) {
				ColumnComparison cc = (ColumnComparison) pred;
				ResultColumn column1 = cc.getColumn1();
				ResultColumn column2 = cc.getColumn2();

				if (column1.getTable().getParent() == column2.getTable()
						&& column1.getRefColumn() == column2.getColumn()
						&& column1.isImplicit()) {
					if (hierarchical)
						column1.getEntity().setParent(column2.getEntity());

					column1.getEntity().setRemapped(false);
					column1.getEntity().setRefJoinColumn(column2);
					cc.setOmit(true);
				} else if (column2.getTable().getParent() == column1.getTable()
						&& column2.getRefColumn() == column1.getColumn()
						&& column2.isImplicit()) {
					if (hierarchical)
						column2.getEntity().setParent(column1.getEntity());

					column2.getEntity().setRemapped(false);
					column2.getEntity().setRefJoinColumn(column1);
					cc.setOmit(true);
				}
			}

		// 2) Keep with document mapping by searching for join predicates
		// between tables already mapped in document (but not yet mapped for
		// output,
		// to exclude tables processed in previous for-loop)
		for (Predicate pred : predicate.getPredicates())
			if (pred instanceof ColumnComparison) {
				ColumnComparison cc = (ColumnComparison) pred;
				ResultColumn column1 = cc.getColumn1();
				ResultColumn column2 = cc.getColumn2();
				Entity entity1 = column1.getEntity();
				Entity entity2 = column2.getEntity();

				if (entity1.getParent() == null
						&& entity1.getRefJoinColumn() == null
						&& column1.getTable().getParent() == column2.getTable()
						&& column1.getRefColumn() == column2.getColumn()) {
					if (hierarchical && !entity1.isAncestorOf(entity2))
						entity1.setParent(entity2);

					entity1.setRemapped(true);
					entity1.setRefJoinColumn(column2);
					cc.setOmit(true);
				} else if (entity2.getParent() == null
						&& entity2.getRefJoinColumn() == null
						&& column2.getTable().getParent() == column1.getTable()
						&& column2.getRefColumn() == column1.getColumn()) {
					if (hierarchical && !entity2.isAncestorOf(entity1))
						entity2.setParent(entity1);

					entity2.setRemapped(true);
					entity2.setRefJoinColumn(column1);
					cc.setOmit(true);
				}
			}

		// 3) Same as 2), but now for join predicates over foreign keys between
		// tables not mapped via that key in document
		for (Predicate pred : predicate.getPredicates())
			if (pred instanceof ColumnComparison) {
				ColumnComparison cc = (ColumnComparison) pred;
				ResultColumn column1 = cc.getColumn1();
				ResultColumn column2 = cc.getColumn2();
				Entity entity1 = column1.getEntity();
				Entity entity2 = column2.getEntity();

				if (entity1.getParent() == null
						&& entity1.getRefJoinColumn() == null
						&& column1.getRefColumn() == column2.getColumn()) {
					if (hierarchical && !entity1.isAncestorOf(entity2))
						entity1.setParent(entity2);

					entity1.setRemapped(true);
					entity1.setRefJoinColumn(column2);
					cc.setOmit(true);
				} else if (entity2.getParent() == null
						&& entity2.getRefJoinColumn() == null
						&& column2.getRefColumn() == column1.getColumn()) {
					if (hierarchical && !entity2.isAncestorOf(entity1))
						entity2.setParent(entity1);

					entity2.setRemapped(true);
					entity2.setRefJoinColumn(column1);
					cc.setOmit(true);
				}
			}

		// Test if entities form a single tree, if not remove hierarchy
		if (hierarchical) {
			int unconnected = 0;
			for (Entity entity : entities.values())
				if (entity.getParent() == null)
					unconnected++;
			if (unconnected > 1) {
				hierarchical = false;
				for (Entity entity : entities.values())
					entity.setParent(null);
			}
		}
	}

	private String getForAndLetClause() {
		String fromWhere = "";
		boolean startWithFor = true;

		for (Entity entity : sortEntities()) {
			if (startWithFor)
				fromWhere += "for ";
			else
				fromWhere += ",\n\t";
			startWithFor = false;

			fromWhere += String.format("$%s in %s", entity.getAlias(),
					entity.getXPathSelector());
			if (entity.getChildren().size() > 0) {
				String letClause = getLetClause(entity, "\t");
				if (!letClause.isEmpty()) {
					fromWhere += "\nlet " + letClause;
					startWithFor = true;
				}
			}
		}
		return fromWhere;
	}

	private String getLetClause(Entity entity, String indent) {
		String result = "";
		if (projection.isProjected(entity)
				|| (orderBy != null && orderBy.determinesOrder(entity))) {
			for (Entity child : entity.getChildren()) {
				if (!result.isEmpty())
					result += ",\n" + indent;

				result += String.format("$%s := %s", child.getAlias(),
						child.getXPathSelector());

				String childLetClause = getLetClause(child, indent + "\t");
				if (!childLetClause.isEmpty())
					result += ",\n" + indent + "\t" + childLetClause;
			}
		}

		return result;
	}

	private List<Entity> sortEntities() {
		List<Entity> entitiesSorted = new ArrayList<Entity>();
		List<Entity> tmp = new ArrayList<Entity>(entities.values());

		while (!tmp.isEmpty()) {
			Iterator<Entity> it = tmp.iterator();
			while (it.hasNext()) {
				Entity entity = it.next();
				if (entity.getParent() != null) {
					tmp.remove(entity);
					break;
				} else if (entity.getRefJoinColumn() == null
						|| entitiesSorted.contains(entity.getRefJoinColumn()
								.getEntity())) {
					entitiesSorted.add(entity);
					tmp.remove(entity);
					break;
				}
			}
		}

		return entitiesSorted;
	}

	public void setEntities(LinkedHashMap<String, Entity> entities) {
		this.entities = entities;
	}
}
